package conf

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"os"
	"sync"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	gorm_mysql "gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 配置对象的默认实例
func DefaultConfig() *Config {
	return &Config{
		App:   newDefaultApp(),
		Http:  newDefaultHttp(),
		Mysql: newDefaultMysql(),
		Mongo: newDefaultMongoDB(),
		Grpc:  NewDefaultGrpc(),
	}
}

type Config struct {
	App   *App     `json:"app" toml:"app"`
	Http  *Http    `json:"http" toml:"http"`
	Mysql *Mysql   `json:"mysql" toml:"mysql"`
	Mongo *mongodb `json:"mongo" toml:"mongo"`
	// [grpc]
	Grpc *Grpc `json:"grpc" toml:"grpc"`
}

func (c *Config) String() string {
	d, _ := json.MarshalIndent(c, "", " ")
	return string(d)
}

func newDefaultApp() *App {
	return &App{
		Name: "mcenter",
	}
}

// 程序的配置
type App struct {
	Name string `json:"name" toml:"name" env:"APP_NAME"`
}

// Mysql的配置
// Mysql的构造函数
func newDefaultMysql() *Mysql {
	return &Mysql{
		Host:     "127.0.0.1",
		Port:     3306,
		Username: "root",
		Password: "root",
		DBName:   "mcenter",
	}
}

// Mysql配置的结构体
type Mysql struct {
	Host     string `json:"host" toml:"host" env:"MYSQL_HOST"`
	Port     int    `json:"port" toml:"port" env:"MYSQL_PORT"`
	Username string `json:"username" toml:"username" env:"MYSQL_USERNAME"`
	Password string `json:"password" toml:"password" env:"MYSQL_PASSWORD"`
	DBName   string `json:"db_name" toml:"db_name" env:"MYSQL_DB_NAME"`

	// 高级参数
	// 最大连接数
	MaxOpenConn int `toml:"max_open_conn" env:"MYSQL_MAX_OPEN_CONN"`
	MaxIdleConn int `toml:"max_idle_conn" env:"MYSQL_MAX_IDLE_CONN"`
	MaxLifeTime int `toml:"max_life_time" env:"MYSQL_MAX_LIFE_TIME"`
	MaxIdleTime int `toml:"max_idle_time" env:"MYSQL_MAX_IDLE_TIME"`

	// 面临并发安全
	lock sync.Mutex
	db   *gorm.DB
}

// 因为需要自定义一些连接池的设置,所以需要GORM通过一个现有的数据连接初始化
// 自定义一个连接
func (m *Mysql) GetConnPool() (*sql.DB, error) {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		m.Username, m.Password, m.Host, m.Port, m.DBName)
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return nil, fmt.Errorf("连接mysql<%s>失败: %s", dsn, err.Error())
	}
	db.SetMaxIdleConns(m.MaxIdleConn)
	db.SetMaxOpenConns(m.MaxOpenConn)
	if m.MaxIdleTime > 0 {
		db.SetConnMaxIdleTime(time.Second * time.Duration(m.MaxIdleTime))
	}
	if m.MaxLifeTime > 0 {
		db.SetConnMaxLifetime(time.Second * time.Duration(m.MaxLifeTime))
	}
	ctx, cancle := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancle()
	if err := db.PingContext(ctx); err != nil {
		return nil, fmt.Errorf("ping mysql<%s> err: %s", dsn, err.Error())
	}
	return db, nil
}

// 根据创建好的连接池,初始化gorm.db
func (m *Mysql) ORM() *gorm.DB {
	m.lock.Lock()
	defer m.lock.Unlock()
	if m.db == nil {
		//初始化DB
		// 1获取sql.db
		pool, err := m.GetConnPool()
		if err != nil {
			panic(err)
		}
		// 1.2 使用pool 初始化orm db对象
		m.db, err = gorm.Open(gorm_mysql.New(gorm_mysql.Config{
			Conn: pool,
		}), &gorm.Config{
			// 执行任何SQL 时都创建并缓存预编译语句,可以提高后续的调用速度
			PrepareStmt: true,
			// 对于写操作(创建, 更新, 删除), 为了确保数据的完整性,GOR 会将它们封装在事物内运行.
			// 但是这会降低性能, 如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约30%+ 性能
			SkipDefaultTransaction: true,
			// 更有效地插入大量记录, 请将一个slice 传递给Create方法
			//CreateBatchSize: 200,
		})
		if err != nil {
			panic(fmt.Errorf("初始化gorm db对象失败: %s", err.Error()))
		}
	}
	// 将db对象绑定到config中的mysql对象中
	return m.db
}

func (m *Mysql) Close() error {
	if m.db == nil {
		return nil
	}
	// 没有提供Clos方法
	// 没法实现数据库的优雅关闭
	return nil
}

// Http地址的配置
// Http的构造函数
func newDefaultHttp() *Http {
	return &Http{
		Host: "0.0.0.0",
		Port: 19999,
	}
}

// Http配置的结构体
type Http struct {
	Host string `json:"host" toml:"host" env:"HTTP_HOST"`
	Port int    `json:"port" toml:"port" env:"HTTP_PORT"`
}

// Http的获取Address字符串方法
func (h Http) Address() string {
	return fmt.Sprintf("%s:%d", h.Host, h.Port)
}

// Grpc的配置
func NewDefaultGrpc() *Grpc {
	return &Grpc{
		Host: "0.0.0.0",
		Port: 18800,
	}
}

type Grpc struct {
	Host string `json:"host" toml:"host" env:"GRPC_HOST"`
	Port int    `json:"port" toml:"port" env:"GRPC_PORT"`
}

func (h Grpc) Address() string {
	return fmt.Sprintf("%s:%d", h.Host, h.Port)
}

func newDefaultMongoDB() *mongodb {
	m := &mongodb{
		UserName:       "mcenter",
		Password:       "123456",
		Database:       "mcenter_mini",
		AuthDB:         "mcenter_mini",
		Endpoints:      []string{"192.168.95.200:27017"},
		K8sServiceName: "MONGODB",
	}
	m.LoadK8sEnv()
	return m
}

type mongodb struct {
	Endpoints      []string `toml:"endpoints" env:"MONGO_ENDPOINTS" envSeparator:","`
	UserName       string   `toml:"username" env:"MONGO_USERNAME"`
	Password       string   `toml:"password" env:"MONGO_PASSWORD"`
	Database       string   `toml:"database" env:"MONGO_DATABASE"`
	AuthDB         string   `toml:"auth_db" env:"MONGO_AUTH_DB"`
	K8sServiceName string   `toml:"k8s_service_name" env:"K8S_SERVICE_NAME"`

	client *mongo.Client
	lock   sync.Mutex
}

// 当 Pod 运行在 Node 上，kubelet 会为每个活跃的 Service 添加一组环境变量。
// kubelet 为 Pod 添加环境变量 {SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT。
// 这里 Service 的名称需大写，横线被转换成下划线
// 具体请参考: https://kubernetes.io/zh-cn/docs/concepts/services-networking/service/#environment-variables
func (m *mongodb) LoadK8sEnv() {
	host := os.Getenv(fmt.Sprintf("%s_SERVICE_HOST", m.K8sServiceName))
	port := os.Getenv(fmt.Sprintf("%s_SERVICE_PORT", m.K8sServiceName))
	addr := fmt.Sprintf("%s:%s", host, port)
	if host != "" && port != "" {
		m.Endpoints = []string{addr}
	}
}

func (m *mongodb) GetAuthDB() string {
	if m.AuthDB != "" {
		return m.AuthDB
	}

	return m.Database
}

func (m *mongodb) GetDB() (*mongo.Database, error) {
	conn, err := m.Client()
	if err != nil {
		return nil, err
	}
	return conn.Database(m.Database), nil
}

// 关闭数据库连接
func (m *mongodb) Close(ctx context.Context) error {
	if m.client == nil {
		return nil
	}

	return m.client.Disconnect(ctx)
}

// Client 获取一个全局的mongodb客户端连接
func (m *mongodb) Client() (*mongo.Client, error) {
	// 加载全局数据量单例
	m.lock.Lock()
	defer m.lock.Unlock()

	if m.client == nil {
		conn, err := m.getClient()
		if err != nil {
			return nil, err
		}
		m.client = conn
	}

	return m.client, nil
}

func (m *mongodb) getClient() (*mongo.Client, error) {
	opts := options.Client()

	if m.UserName != "" && m.Password != "" {
		cred := options.Credential{
			AuthSource: m.GetAuthDB(),
		}

		cred.Username = m.UserName
		cred.Password = m.Password
		cred.PasswordSet = true
		opts.SetAuth(cred)
	}
	opts.SetHosts(m.Endpoints)
	opts.SetConnectTimeout(5 * time.Second)
	// opts.Monitor = otelmongo.NewMonitor(
	// 	otelmongo.WithCommandAttributeDisabled(true),
	// )

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*5))
	defer cancel()

	// Connect to MongoDB
	client, err := mongo.Connect(ctx, opts)
	if err != nil {
		return nil, fmt.Errorf("new mongodb client error, %s", err)
	}

	if err = client.Ping(ctx, nil); err != nil {
		return nil, fmt.Errorf("ping mongodb server(%s) error, %s", m.Endpoints, err)
	}

	return client, nil
}
